/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.extension.security.internal.analyzer;

import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.solr.client.solrj.SolrServerException;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.extension.CoreExtension;
import org.xwiki.extension.Extension;
import org.xwiki.extension.InstalledExtension;
import org.xwiki.extension.index.internal.ExtensionIndexStore;
import org.xwiki.extension.index.security.ExtensionSecurityAnalysisResult;
import org.xwiki.extension.index.security.SecurityVulnerabilityDescriptor;
import org.xwiki.extension.index.security.review.Review;
import org.xwiki.extension.index.security.review.ReviewResult;
import org.xwiki.extension.index.security.review.ReviewsMap;
import org.xwiki.extension.repository.CoreExtensionRepository;

import static org.apache.commons.text.StringEscapeUtils.escapeXml11;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
import static org.xwiki.extension.index.security.review.ReviewResult.UNSAFE;
import static org.xwiki.extension.security.internal.ExtensionSecurityAdvice.TRANSITIVE_DEPENDENCY_ADVICE;
import static org.xwiki.extension.security.internal.ExtensionSecurityAdvice.UPGRADE_ENVIRONMENT_ADVICE;
import static org.xwiki.extension.security.internal.ExtensionSecurityAdvice.UPGRADE_FROM_EM_ADVICE;
import static org.xwiki.extension.security.internal.ExtensionSecurityAdvice.UPGRADE_XWIKI_ADVICE;

/**
 * Update an {@link Extension} with the results of its latest {@link ExtensionSecurityAnalysisResult}.
 *
 * @version $Id$
 * @since 15.5RC1
 */
@Component(roles = VulnerabilityIndexer.class)
@Singleton
public class VulnerabilityIndexer
{
    @Inject
    private ExtensionIndexStore extensionIndexStore;

    @Inject
    private CoreExtensionRepository coreExtensionRepository;

    @Inject
    private Logger logger;

    /**
     * Update the security vulnerabilities known for the given extension.
     *
     * @param extension the extension to update
     * @param analysis the security analysis to update the extension
     * @param reviewsMap the map of reviewed CVEs
     * @return {@code true} if some new security vulnerabilities are inserted, {@code false} otherwise
     */
    public boolean update(Extension extension, ExtensionSecurityAnalysisResult analysis, ReviewsMap reviewsMap)
    {
        try {
            List<String> cveiDs = this.extensionIndexStore.getCVEIDs(extension.getId());

            List<SecurityVulnerabilityDescriptor> securityVulnerabilities = analysis.getSecurityVulnerabilities();
            boolean hasNew = securityVulnerabilities.stream()
                .anyMatch(vulnerability -> {
                    String vulnerabilityId = vulnerability.getId();
                    Set<String> aliases = vulnerability.getAliases();
                    return !cveiDs.contains(vulnerabilityId)
                        // To avoid raising a notification for a vulnerability that was already notified with another id
                        && cveiDs.stream().noneMatch(aliases::contains)
                        && isNotSafe(reviewsMap, vulnerabilityId);
                });

            analysis.setCoreExtension(extension instanceof CoreExtension);
            if (!securityVulnerabilities.isEmpty()) {
                if (extension instanceof CoreExtension) {
                    if (isFromEnvironment((CoreExtension) extension)) {
                        analysis.setAdvice(UPGRADE_ENVIRONMENT_ADVICE.getTranslationId());
                    } else {
                        analysis.setAdvice(UPGRADE_XWIKI_ADVICE.getTranslationId());
                    }
                } else if (isDirectDependency(extension)) {
                    analysis.setAdvice(TRANSITIVE_DEPENDENCY_ADVICE.getTranslationId());
                } else {
                    analysis.setAdvice(UPGRADE_FROM_EM_ADVICE.getTranslationId());
                }
            }

            securityVulnerabilities.forEach(securityVulnerabilityDescriptor -> {
                Optional<List<Review>> byId = reviewsMap.getById(securityVulnerabilityDescriptor.getId());
                if (byId.isPresent()) {
                    securityVulnerabilityDescriptor.setReviews(formatReviews(byId.get()));
                } else {
                    securityVulnerabilityDescriptor.setReviews("");
                }
                securityVulnerabilityDescriptor
                    .setSafe(!isNotSafe(reviewsMap, securityVulnerabilityDescriptor.getId()));
            });

            this.extensionIndexStore.update(extension.getId(), analysis);

            return hasNew;
        } catch (SolrServerException | IOException e) {
            this.logger.warn(
                "Failed to update the extension [{}] with the results of its latest security scan. Cause: [{}]",
                extension.getId(), getRootCauseMessage(e));
            return false;
        }
    }

    /**
     * Check if the given extension is a direct dependency. An extension is not a direct dependency if it is only
     * installed because it is part of the dependencies of another extension.
     *
     * @param extension the extension to check
     * @return {@code true} if the extension is a direct dependency, {@code false} otherwise
     */
    private static boolean isDirectDependency(Extension extension)
    {
        return extension instanceof InstalledExtension
            && ((InstalledExtension) extension).isDependency(null);
    }

    private static String formatReviews(List<Review> reviews)
    {
        String reviewsFormatted = reviews.stream()
            .filter(it -> it.getResult() == ReviewResult.SAFE)
            .map(VulnerabilityIndexer::formatReview).collect(Collectors.joining());
        boolean hasUnsafe = reviews.stream().anyMatch(review -> review.getResult() == UNSAFE);
        if (hasUnsafe) {
            String unsafeReviews = reviews
                .stream()
                .filter(review -> review.getResult() == UNSAFE)
                .map(VulnerabilityIndexer::formatReview)
                .collect(Collectors.joining());
            reviewsFormatted =
                String.format("<div class='box errormessage'>%s</div>%s", unsafeReviews, reviewsFormatted);
        }
        return reviewsFormatted;
    }

    private static boolean isNotSafe(ReviewsMap reviewsMap, String vulnerabilityId)
    {
        boolean isNotSafe;
        Map<String, List<Review>> map = reviewsMap.getReviewsMap();
        if (map.containsKey(vulnerabilityId)) {
            isNotSafe = map.get(vulnerabilityId)
                .stream()
                .anyMatch(review -> review.getResult() == UNSAFE);
        } else {
            isNotSafe = true;
        }
        return isNotSafe;
    }

    private static String formatReview(Review review)
    {
        return String.format("<dl>\n"
                + "  <dt>%s</dt>\n"
                + "  <dd>%s</dd>\n"
                + "</dl>",
            escapeXml11(review.getEmitter()),
            escapeXml11(review.getExplanation()));
    }

    private boolean isFromEnvironment(CoreExtension extension)
    {
        URL warUrl = this.coreExtensionRepository.getEnvironmentExtension().getURL();
        URL url = extension.getURL();
        return !url.toString().startsWith(warUrl.toString());
    }
}
